#!/usr/bin/env python3
import argparse, os, sys, errno, getopt, re
import shutil
from urllib.request import urlopen
from urllib.parse import urljoin
import tarfile, tempfile
import fileinput, subprocess
source_dir = os.path.abspath(os.path.dirname(__file__))
sys.path.append(source_dir + '/../')
from lib import bpar as ar
from lib import bpgit as git
from lib import bpreqs as reqs

KPATH="http://kernel.ubuntu.com/~kernel-ppa/mainline/"
BACKPORTS_KSRC_DIR="ksrc-backports"
# ~ 101 MiB for installed space on usr/src/ and lib/modules/
SPACE_PER_KERNEL=101
# ~13 MiB of both deb files
SPACE_PER_KERNEL_DEB=13

MIN_VERSION = (3, 10)

GIT_TREES = [
    "git://git.kernel.org/pub/scm/linux/kernel/git/backports/backports.git",
    "git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git",
    "git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git",
    "git://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git",
]

def make_version_tuple(ver):
    ver = re.split(r'[.-]', ver)
    sver = []
    for n in ver:
        try:
            sver.append(int(n))
        except ValueError:
            sver.append(-1)
    return sver

def parse_ubuntu_ppa_page(version, baseurl):
    html = urlopen(baseurl).read()
    if b'Build for amd64 failed' in html:
        return None
    pkgs = re.findall(b'href="linux-headers-[^"]*-generic_[^"]*\.deb"', html)
    if not pkgs:
        return None
    pkgs = re.findall(b'href="linux-headers-[^"]*_all\.deb"', html)
    if not pkgs:
        return None
    pkgs = [p[6:-1] for p in pkgs]
    pkgs.sort()
    pkg = pkgs[-1].decode('ascii')
    return (version, baseurl + pkg)

class backport_kernel_updater:
    """
    This is the Linux kernel backports kernel updater. It ensures you
    have your system up to date with the latest expected kernels, it
    will also remove any stale kernels directories or files that the
    upstream project no longer needs or wants.
    """
    def pkg_arch(self, machine):
        if machine == "x86_64":
            return "amd64"
        elif machine == "i686":
            return "i386"
        sys.stdout.write("Unsupported machine type: %s\n" % machine)
        sys.exit(1)

    @classmethod
    def _get_ubuntu_ppa_mainline_kernels(cls, pkg_arch):
        base = KPATH
        html = urlopen(base).read()
        builds = re.findall(b'href="([-v.0-9a-z]*)/"', html)
        builds = [b.decode('ascii') for b in builds]
        # remove some special kernels
        builds = [b for b in builds if re.match('v[3-9]', b)]
        builds = [b for b in builds if not '-rc' in b and not '-unstable' in b and not '-ckt' in b]
        last = {}
        for b in builds:
            split = re.split(r'[.-]', b, 2)
            ver = '.'.join(split[:2])
            sub = b[len(ver):]
            if not sub.startswith('.'):
                sub = '.0' + sub
            sub = sub[1:]
            sver = make_version_tuple(sub)
            item = (sver, b)
            try:
                last[ver].append(item)
            except KeyError:
                last[ver] = [item]
        for ver in last:
            n = 0
            for sver in last[ver]:
                if len(sver[0]) > n:
                    n = len(sver[0])
            for sver in last[ver]:
                while len(sver[0]) < n:
                    sver[0].append(0)

        filtered = {}
        for subvers in last:
            if tuple(make_version_tuple(subvers[1:])) >= MIN_VERSION:
                filtered[subvers] = last[subvers]
        last = filtered

        result = []
        for subvers in last.values():
            subvers.sort(reverse=True)
            for version in subvers:
                version = version[1]

                baseurl = base + version + '/'
                pkgs = parse_ubuntu_ppa_page(version, baseurl)
                if pkgs:
                    result.append(pkgs)
                    break
                baseurl = base + version + '/' + pkg_arch + '/'
                pkgs = parse_ubuntu_ppa_page(version, baseurl)
                if pkgs:
                    result.append(pkgs)
                    break

        result.sort(key = lambda x: make_version_tuple(x[0][1:]))
        return [x[1] for x in result]

    def __init__(self, yes=False, no_keep=False,
                 git_trees_only=False,
                 reference=None):
        self.ksrc_base = ""
        self.ksrc_prefix = ""
        sysname, nodename, release, version, self.machine = os.uname()
        self.pkg_arch = self.pkg_arch(self.machine)
        self.kernel_urls = self._get_ubuntu_ppa_mainline_kernels(self.pkg_arch)
        self.num_kernels = len(self.kernel_urls)
        self.git_trees = GIT_TREES
        self.git_trees_missing = list()
        self.git_trees_present = list()
        self.all_git_trees_present = True
        self.kernel_urls_generic = list()
        self.kernel_bases = list()
        self.kernel_pkgs = list()
        self.kernel_vers = list()
        self.kernel_vers_count = list()
        self.root = os.geteuid() == 0
        self.yes = yes
        self.no_keep = no_keep
        self.git_trees_only = git_trees_only
        self.reference = reference
        self.reference_git = None
        self.cwd = os.getcwd()
        if self.root:
            self.ksrc_base = self.cwd + "/"
            self.ksrc_prefix = self.ksrc_base + BACKPORTS_KSRC_DIR
        else:
            self.ksrc_base = os.environ['HOME'] + "/"
            self.ksrc_prefix = self.ksrc_base + BACKPORTS_KSRC_DIR
        if not self.reference:
            self.reference = os.path.join(self.ksrc_base, 'linux/.git')
        else:
            if not os.path.isdir(self.reference):
                sys.stderr.write("Reference directory does not exist: %s\n" % self.reference)
                sys.exit(1)
            else:
                base_dir = os.path.dirname(self.reference)
                self.reference_git = dict(git_url=None,
                                          directory=base_dir,
                                          directory_obj=self.reference,
                                          directory_valid=True,
                                          tree_up_to_date=False,
                                          reference_args=list())
        for kernel_url in self.kernel_urls:
            pkg = kernel_url.split('/')[-1]
            self.kernel_pkgs.append(pkg)
            version = pkg.split('-')[2]
            self.kernel_vers.append(version)
            kver = dict(ver = version, count = 0)
            self.kernel_vers_count.append(kver)
            pkg_tmp = pkg.split('-')[2] + "-" + pkg.split('-')[3]
            pkg_tmp_base = pkg_tmp.split('_')[0]
            self.kernel_bases.append(pkg_tmp_base)
            m = re.match(r"(?P<part_1>.*)_(?P<part_2>.*)_all.deb", kernel_url)
            pkg_generic_file = m.group('part_1') + '-generic_' + \
                               m.group('part_2') + '_' + self.pkg_arch + '.deb'
            pkg_generic_url = urljoin(kernel_url, pkg_generic_file)
            self.kernel_urls_generic.append(pkg_generic_url)

        for git_tree in self.git_trees:
            tree_name = git_tree.split('/')[-1]
            tree_dir = tree_name.split('.')[0]
            tree_dir = os.path.join(self.ksrc_base, tree_dir.split('.')[0])
            tree_dir_objs = os.path.join(tree_dir + '/.git')
            tree_ok = os.path.isdir(tree_dir) and os.path.isdir(tree_dir_objs)
            tree = dict(git_url=git_tree,
                        directory=tree_dir,
                        directory_obj=tree_dir_objs,
                        directory_valid=tree_ok,
                        tree_up_to_date=False,
                        reference_args=list())
            if not self.reference_git and tree_name in self.reference.replace("/", ""):
                self.reference_git = tree
                continue
            if not tree_ok:
                self.git_trees_missing.append(tree)
            else:
                self.git_trees_present.append(tree)

        for git_tree in self.git_trees_present + self.git_trees_missing:
            if 'backports' in git_tree.get('directory'):
                continue
            if not git_tree.get('directory_valid'):
                git_tree['reference_args'].append("--reference")
                git_tree['reference_args'].append(self.reference_git.get('directory_obj'))

        if self.reference_git.get('directory_valid'):
            self.all_git_trees_present = (1 + len(self.git_trees_present)) == len(self.git_trees)
        else:
            self.all_git_trees_present = len(self.git_trees_present) == len(self.git_trees)

        urls = self.kernel_urls + self.kernel_urls_generic
        self.all_kernel_urls = urls
        self.all_new_kernels = list()

    def warn_root(self):
        if not self.root:
            return
        sys.stdout.write("** Running as a privileged user!\n")
        sys.stdout.write("** You are trying to force using %s and %s ...\n" % (
                         self.ksrc_prefix + "/lib/modules",
                         self.ksrc_prefix + "/usr/src"))
        sys.stdout.write("** This is a terrible idea. Consider running " \
                         "as a non root.\n")
        if not self.yes:
            answer = input("Do you still want to continue (y/N)? ")
            if answer != "y":
                sys.exit(1)
    def warn_size_reqs_about(self):
        sys.stdout.write("\n")
        sys.stdout.write("Linux kernel backports updater\n")
        sys.stdout.write("------------------------------------------------------------------\n")
        sys.stdout.write("There are two parts to the updater:\n\n")
        sys.stdout.write("\t1. Stable kernel header release updater\n")
        sys.stdout.write("\t2. Git tree updater\n\n")
        sys.stdout.write("Step 1) will go first and will be immediately followed by Step 2).\n")
        sys.stdout.write("A description of what will be done and what space requires are needed\n")
        sys.stdout.write("are described below. Total expected available space: 6 GiB.\n")
    def warn_size_reqs_kernels(self):
        sys.stdout.write("\n")
        sys.stdout.write("Stable kernel header release updater\n")
        sys.stdout.write("------------------------------------------------------------------\n")
        sys.stdout.write("This will download %d kernel headers to allow you to\n" % self.num_kernels)
        sys.stdout.write("cross compile any module over these kernels with ckmake.\n")
        sys.stdout.write("The download payload is about ~ %d MiB, once uncompressed\n" %
                         (SPACE_PER_KERNEL_DEB * self.num_kernels))
        sys.stdout.write("it will stash kernel header files under the directories:\n\n\t%s\n\t%s\n\n" %
                         (self.ksrc_prefix + "/usr/src/", self.ksrc_prefix + "/lib/modules/"))
        sys.stdout.write("It will consume about ~ %d GiB of space.\n\n" % (self.num_kernels * SPACE_PER_KERNEL / 1024))
        sys.stdout.write("The kernel headers used are from Vanilla kernels")
        sys.stdout.write("from the \nUbuntu mainline / vanilla kernel PPA and are extracted\n")
        sys.stdout.write("using the GNU ar and Python tar module:\n\n%s\n\n" % KPATH)
    def warn_size_reqs_git_trees(self):
        sys.stdout.write("\n")
        sys.stdout.write("Git tree updater\n")
        sys.stdout.write("------------------------------------------------------------------\n")
        sys.stdout.write("This will download or update all backport related kernel git trees:\n\n")
        for git_tree in self.git_trees:
            sys.stdout.write("\t%s\n" % git_tree)
        sys.stdout.write("\n")
        sys.stdout.write("By default we will git clone or expect each git tree to be present under:\n")
        sys.stdout.write("\n\n\t%s\n\n" % self.ksrc_base)
        sys.stdout.write("For each git clone issused --reference will be used based on Linus'\n")
        sys.stdout.write("linux.git tree to help save as much space as possible. If you do not\n")
        sys.stdout.write("have the git trees cloned you will be expected to download over 1 GiB\n")
        sys.stdout.write("of data and once the trees are extracted they will consume about\n")
        sys.stdout.write("2-3 GiB of space.\n")
        sys.stdout.write("\n")
        if self.all_git_trees_present:
            sys.stdout.write("We detected you have all git trees present, we'll just git fetch for you.\n\n");
        else:
            if self.git_trees_missing:
                sys.stdout.write("Missing trees:\n")
                for git_tree in self.git_trees_missing:
                    sys.stdout.write("%s\n" % (git_tree.get('git_url')))
            if self.git_trees_present:
                sys.stdout.write("Present trees:\n")
                for git_tree in self.git_trees_present:
                    sys.stdout.write("%s\n" % (git_tree.get('directory')))
            sys.stdout.write("\n")
    def warn_size_reqs_prompt(self):
        answer = input("Do you still want to continue (y/N)? ")
        if answer != "y":
            sys.exit(1)
        sys.stdout.write("\n")
    def warn_size_reqs(self):
        if self.yes:
            return
        if not self.git_trees_only:
            self.warn_size_reqs_about()
        if not self.git_trees_only:
            self.warn_size_reqs_kernels()
        self.warn_size_reqs_git_trees()
        self.warn_size_reqs_prompt()
    def move_dir(self, dirname, dir_target):
        old_ksrc_prefix = self.ksrc_base + dir_target
        if (os.path.islink(old_ksrc_prefix)):
            old_real_path=os.path.realpath(old_ksrc_prefix)
            old_base_dir=os.path.dirname(old_real_path)
            new_real_path = old_base_dir + BACKPORTS_KSRC_DIR
            try:
                os.rename(old_base_dir + "compat-ksrc", new_real_path)
                os.unlink(old_ksrc_prefix)
                os.symlink(old_real_path, new_real_path)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
        elif (os.path.isdir(old_ksrc_prefix)):
            try:
                os.rename(old_ksrc_prefix, self.ksrc_prefix)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
    def move_old_compat_dirs(self):
        if self.git_trees_only:
            return
        old_ksrc_prefix = self.ksrc_base + "compat-ksrc"
        if os.path.islink(old_ksrc_prefix):
            old_real_path=os.path.realpath(old_ksrc_prefix)
            old_base_dir=os.path.dirname(old_real_path)
            new_real_path = old_base_dir + "/" + BACKPORTS_KSRC_DIR
            try:
                sys.stdout.write("Trying to mv %s %s\n\n" %(old_base_dir + "/compat-ksrc", new_real_path))
                os.rename(old_base_dir + "/compat-ksrc", new_real_path)
                os.unlink(old_ksrc_prefix)
                os.symlink(new_real_path, self.ksrc_prefix)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
        elif os.path.isdir(old_ksrc_prefix):
            try:
                os.rename(old_ksrc_prefix, self.ksrc_prefix)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
        old_debs = self.ksrc_base + "debs"
        if os.path.islink(old_debs):
            old_real_path=os.path.realpath(old_debs)
            old_base_dir=os.path.dirname(old_real_path)
            new_real_path = old_base_dir + "/" + BACKPORTS_KSRC_DIR + "/" + "debs"
            try:
                sys.stdout.write("Trying to mv %s %s\n\n" %(old_base_dir + "/debs", new_real_path))
                os.rename(old_base_dir + "/debs", new_real_path)
                os.unlink(old_debs)
                os.symlink(old_real_path, new_real_path)
                os.symlink(old_deb, new_real_path)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
        elif os.path.isdir(old_debs):
            try:
                os.rename(old_debs, self.ksrc_prefix + "debs")
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
    def prep_dirs(self):
        if self.git_trees_only:
            return
        for i in [self.ksrc_prefix,
                  self.ksrc_prefix + "/debs",
                  self.ksrc_prefix + "/usr/src",
                  self.ksrc_prefix + "/lib/modules"]:
            try:
                path = os.path.join(self.ksrc_prefix, i)
                if not os.path.isdir(path):
                    sys.stdout.write("Creating %s\n" % path)
                    os.makedirs(path)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    raise
    def is_stale_kernel_str(self, pkg):
        for kbase in self.kernel_bases:
            if (kbase in pkg):
                return False
        return True
    def remove_stale_kernel_pkgs(self):
        pkg_path = os.path.join(self.ksrc_prefix, 'debs')
        for root, dirs, files in os.walk(pkg_path):
            for f in files:
                if not f.endswith('.deb'):
                    continue
                if self.is_stale_kernel_str(f):
                    pkg_full_path = os.path.join(pkg_path, f);
                    sys.stdout.write("Removing old kpkg: %s\n" % pkg_full_path)
                    os.remove(pkg_full_path)
        return 0
    def rm_clean_dir(self, d):
        if not os.path.isdir(d):
            return
        shutil.rmtree(d, ignore_errors=True)
        try:
            os.rmdir(d)
        except OSError as e:
            if e.errno != errno.ENOENT:
                raise
    def remove_stale_kernel_dir(self, dir_name):
        dir_path = os.path.join(self.ksrc_prefix, dir_name)
        for stuff in os.listdir(dir_path):
            d = os.path.join(dir_path, stuff)
            if not os.path.isdir(d):
                continue
            if self.is_stale_kernel_str(d):
                dir_full_path = os.path.join(dir_path, d);
                sys.stdout.write("Removing old directory: %s\n" % d)
                self.rm_clean_dir(d)
    def remove_stale_kernel_dirs(self):
        for i in [self.ksrc_prefix + "/usr/src",
                  self.ksrc_prefix + "/lib/modules"]:
            if os.path.isdir(i):
                self.remove_stale_kernel_dir(i)
    def wget_url(self, url, dir_target):
        sys.stdout.write('Trying URL:  %s\n' % url)
        target = os.path.join(dir_target, url.split('/')[-1])
        u = urlopen(url)
        f = open(target, 'wb')
        meta = u.info()
        target_size = int(meta.get_all("Content-Length")[0])
        sys.stdout.write('Target path: %s bytes: %s\n' % (target, target_size))
        target_size_now = os.path.getsize(target)
        check = 8192
        if target_size_now > target_size:
            sys.stdout.write('Filesystem file size (%d) > URL file size (%s) bytes: %s' % (target_size_now, target_size))
            return
        if target_size_now:
            buff = u.read(target_size_now)
            if not buff:
                return
        while True:
            buff = u.read(check)
            if not buff:
                break
            target_size_now += len(buff)
            f.write(buff)
            sys.stdout.write('\r')
            sys.stdout.write("%10d  [%3.2f%%]" % (target_size_now, target_size_now * 100. / target_size))
            sys.stdout.flush()
        f.close()
        sys.stdout.write("\n")
    def url_file_chunks_missing(self, url, dir_target):
        target = os.path.join(dir_target, url.split('/')[-1])
        if not os.path.isfile(target):
            return True
        u = urlopen(url)
        meta = u.info()
        target_size = int(meta.get_all("Content-Length")[0])
        target_size_now = os.path.getsize(target)
        if target_size_now < target_size:
            return True
        return False
    def get_kver(self, url):
        for kver in self.kernel_vers_count:
            if kver.get('ver') in url:
                return kver
        return None
    def increase_kver_count(self, url):
        kver = self.get_kver(url)
        if not kver:
            return
        kver['count'] = kver['count'] + 1
        if kver['count'] >= 2:
            sys.stdout.write("%s - up to date!\n" % kver.get('ver'))
    def is_new_kernel(self, string):
        if self.no_keep:
            return True
        for kernel in self.all_new_kernels:
            if string in kernel:
                return True
        return False
    def get_backport_pkgs(self):
        sys.stdout.write("Looking for updates and downloading. You can hit CTRL-C\n")
        sys.stdout.write("and come back at any time, we'll take off right where \n")
        sys.stdout.write("we left off\n")
        for url in self.all_kernel_urls:
            dir_target = os.path.join(self.ksrc_prefix, "debs")
            target = os.path.join(dir_target, url.split('/')[-1])
            if not self.url_file_chunks_missing(url, dir_target):
                self.increase_kver_count(url)
                continue
            self.wget_url(url, dir_target)
            self.increase_kver_count(url)
            self.all_new_kernels.append(url)
        sys.stdout.write("\n")
    def fix_header_lib_modules(self, dir_path):
        """
        Relink lib/modules/*/build/ directories relatively to make it work
        for regular user based workflows
        """
        for kernel in os.listdir(dir_path):
            if "generic" not in kernel:
                continue
            kver = self.get_kver(kernel)
            sys.stdout.write("%s - creating lib/modules/$kernel/build symlink for non-root installation ...\n" % (kver.get('ver')))
            src = os.path.join(dir_path, kernel)
            dst = os.path.join(self.ksrc_prefix, 'lib/modules/' + kernel)
            build_target = os.path.join(src, 'build')
            usr_src_sym = "../../../usr/src/linux-headers-" + kernel
            os.unlink(build_target)
            os.symlink(usr_src_sym, build_target)
            shutil.move(src, dst)
    def install_headers(self, dir_path):
        for kernel in os.listdir(dir_path):
            src = os.path.join(dir_path, kernel)
            dst = os.path.join(self.ksrc_prefix, 'usr/src/' + kernel)
            kver = self.get_kver(kernel)
            sys.stdout.write("%s - installing usr/src/ ...\n" % (kver.get('ver')))
            sys.stdout.write("src: %s\n" % src)
            sys.stdout.write("dst: %s\n" % dst)
            self.rm_clean_dir(dst)
            shutil.move(src, dst)
    def sanitize_and_install_kernel_dirs(self, dir_path):
        d_lib_modules = os.path.join(dir_path, "lib/modules")
        d_usr_src     = os.path.join(dir_path, "usr/src")
        if os.path.isdir(d_usr_src):
            self.install_headers(d_usr_src)
        if os.path.isdir(d_lib_modules):
            self.fix_header_lib_modules(d_lib_modules)
    def extract_backport_pkgs(self):
        tmp_prefix = BACKPORTS_KSRC_DIR + '_'
        for url in self.all_kernel_urls:
            tmpdir_path = tempfile.mkdtemp(prefix=tmp_prefix)
            try:
                kver = self.get_kver(url)
                dir_target = os.path.join(self.ksrc_prefix, "debs")
                target = os.path.join(dir_target, url.split('/')[-1])
                if not self.is_new_kernel(url):
                    sys.stdout.write("%s - already installed %s ...\n" % (kver.get('ver'), target))
                    continue
                sys.stdout.write("%s - extracting new %s ...\n" % (kver.get('ver'), target))
                data, dpath = tempfile.mkstemp(prefix=tmp_prefix)
                ar.print_data(target, data)
                tar = tarfile.open(name=dpath, mode='r')
                tar.extractall(path=tmpdir_path, members=bk_tar_members(tar))
                os.unlink(dpath)

                self.sanitize_and_install_kernel_dirs(tmpdir_path)
            finally:
                self.rm_clean_dir(tmpdir_path)
    def rebuild_needed(self, kdir):
        if '-generic' not in kdir:
            return False
        m = re.match(r"^(?P<VERSION>\d+)\.+" \
                     "(?P<PATCHLEVEL>\d+)[.]*", \
                     kdir)
        if not m:
            return False
        rel_specs = m.groupdict()
        if int(rel_specs['VERSION']) < 3:
            return False
        if int(rel_specs['PATCHLEVEL']) == 0:
            return False
        return True
    def make_kdir(self, make_args, kbuild_path):
        tmp_prefix = BACKPORTS_KSRC_DIR + '_rebuild_h_'
        null_file = open('/dev/null', 'r')
        p = subprocess.Popen(['make'] + make_args ,
                             stdin=null_file,
                             stdout=sys.stdout,
                             stderr=sys.stdout,
                             cwd=kbuild_path)
        p.wait()
        null_file.close()
    def clone_git_tree(self, git_tree):
        sys.stdout.write("Cloning tree %s\n" % git_tree.get('git_url'))
        git.clone(git_tree.get('git_url'),
                  git_tree.get('directory'),
                  git_tree.get('reference_args'))
        git_tree['tree_up_to_date']=True
        git_tree['valid']=True
    def update_git_tree(self, git_tree):
        sys.stdout.write("Updating tree %s\n" % git_tree.get('directory'))
        git.fetch(tree=git_tree.get('directory'))
        git_tree['tree_up_to_date']=True
        git_tree['valid']=True
    def clone_or_update_tree(self, git_tree):
        if not git_tree.get('directory_valid'):
            self.clone_git_tree(git_tree)
        else:
            self.update_git_tree(git_tree)
    def update_git_trees(self):
        if self.reference_git:
            git_tree = self.reference_git
            self.clone_or_update_tree(git_tree)
        for git_tree in self.git_trees_present + self.git_trees_missing:
            self.clone_or_update_tree(git_tree)

def bk_tar_members(members):
    for tarinfo in members:
        if not "usr/share" in tarinfo.name:
            yield tarinfo

def _main():
    req = reqs.Req()
    req.require_hint('ar', 'binutils')
    req.require('make')
    req.require('gcc')
    req.require('git')
    if not req.reqs_match():
        sys.exit(1)

    parser = argparse.ArgumentParser(description='Linux kernel backports update manager')
    parser.add_argument('--force', const=True, default=False, action="store_const",
                        help='Alias for --yes --no-keep')
    parser.add_argument('--yes', const=True, default=False, action="store_const",
                        help='Assume yes on all questions')
    parser.add_argument('--no-keep', const=True, default=False, action="store_const",
                        help='Don\'t keep existing installs, overwrite all again')
    parser.add_argument('--git-trees-only', const=True, default=False, action="store_const",
                        help='Only download or update the required git trees')
    parser.add_argument('--reference', metavar='<path-to-git-obj-dir>', type=str,
                        default=None,
                        help='Override what argument to use to git clone --reference')
    args = parser.parse_args()

    bk_updater = backport_kernel_updater(yes=args.yes or args.force,
                                         no_keep=args.no_keep or args.force,
                                         git_trees_only=args.git_trees_only,
                                         reference=args.reference)

    bk_updater.warn_root()
    bk_updater.warn_size_reqs()
    bk_updater.move_old_compat_dirs()
    bk_updater.prep_dirs()

    if bk_updater.git_trees_only:
        bk_updater.update_git_trees()
        return 0

    bk_updater.remove_stale_kernel_pkgs()
    bk_updater.remove_stale_kernel_dirs()
    bk_updater.get_backport_pkgs()
    bk_updater.extract_backport_pkgs()
    bk_updater.update_git_trees()

    return 0

if __name__ == '__main__':
    ret = _main()
if ret:
    sys.exit(ret)
